feat(provider): propagate direnv env to agent sessions#138
Merged
Conversation
Resolve the project's direnv environment (`direnv export json`) and merge it into each provider session's child env (Claude/Codex/Cursor/OpenCode), below the session-injected vars so GITS_PORT (#124) and the git-shim confinement vars always win. The resolver is async and failure-safe (direnv missing, un-allowed .envrc, timeout -> no-op), so session start never breaks or blocks. Fixes the git-shim PATH clobber: allocate() now prepends the shim dir onto the caller's direnv-merged PATH, so the shim stays first (confinement intact) while .envrc PATH_add / nix / mise toolchain dirs survive into the child. Integrated terminal left as-is (its PTY already sources rc files; the login-only /etc/profile gap is a documented follow-up). Closes #134 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #134.
Problem
Agent/provider sessions spawn the provider process directly (not even a non-login shell — a bare
execve), so per-project environment managers (direnv.envrc, mise, nix) never hook in. Agents miss the project's toolchain/env vars that work fine in the user's own terminal.Fix
Resolve the project's direnv environment and merge it into each provider session's child env, below the session-injected vars so
#124'sGITS_PORTand the git-shim confinement vars always win.packages/shared/src/shell.ts— newresolveDirenvEnvironment(cwd, baseEnv)runsdirenv export jsonand returns the env delta (set/unset), plusapplyDirenvDelta. Failure-safe: win32, direnv-not-installed, un-allowed.envrc, timeout, or malformed output all collapse to{}— session start never breaks. It's async (promisify(execFile)) so a slow/hung.envrcnever blocks the Node event loop.apps/server/src/provider/direnvSessionEnv.ts— one shared helper (resolveSessionEnvWithDirenv) used by all four adapters.apps/server/src/provider/Layers/{Claude,Codex,Cursor,OpenCode}Adapter.ts— resolve direnv, then git-shim, then merge session vars last.apps/server/src/provider/GitShimManager.ts— the subtle part. The git-shim prepends its dir to PATH so baregitresolves to the confinement shim. It previously built that PATH from the server's ownprocess.env.PATH, and since the shim env is merged last, it clobbered direnv's PATH additions — silently discardingPATH_add, nix/mise/asdf shims (the whole point of the issue).allocate()now takes an optionalbasePathand prepends the shim dir to the caller's direnv-merged PATH: the shim stays first (confinement intact) and.envrctoolchain dirs survive.Why not
bash -lThe issue floated spawning login shells. Empirically that does not propagate direnv: direnv's
PROMPT_COMMANDhook never fires under-lc/-ilc(verified in-sandbox).direnv export json— the same primitive direnv's own shell hook uses — is the correct mechanism, and it respectsdirenv allow(an un-allowed.envrcexports nothing).#124 interaction (verified)
GITS_PORTand the confinement vars (GITS_REAL_GIT,GITS_ALLOWED_ROOT,GITS_PROTECTED_BRANCHES, shim-first PATH) are all merged after the direnv delta, so a.envrccannot override them. The final child PATH is<shimDir>:<direnv additions>:<original PATH>.Scope
Covers the four provider adapter spawns. The integrated terminal is intentionally not changed — its PTY already sources rc files, so direnv works there via the interactive cd-hook; the login-only gap (
/etc/profile→ nix) is a narrow follow-up, documented with aponytail:comment rather than forcing every terminal into login-shell semantics.Verification
bunx vitest runon the 6 touched suites → 162/162; oxlint (only pre-existing warnings in the untouched shim-script template) / oxfmt clean..envrc, shim dir stays first,direnv allownot bypassed. Added a PATH-survival regression test (CodexAdapter.test.ts) proven to fail against the pre-fix code and pass after.direnvv2.37.1, Playwright, live Claude Sonnet 4.6 turn): a throwaway project with an allowed.envrc(export GITS_DIRENV_PROBE=itworks_134+PATH_add toolchain) → the agent session's Bash tool observed:printenv GITS_DIRENV_PROBE→itworks_134(acceptance: env var visible, no manual wrapping) ✅envtool→envtool-on-path-134(thePATH_addtoolchain reachable — the PATH-clobber fix, live) ✅Follow-ups (not blocking)
/etc/profile-based managers (nix).direnvalone proves insufficient — the resolver's ladder can grow; direnv covers the stated acceptance.